﻿using System;
using System.Collections.Generic;
using System.Text;
using Entropy.Ext.Ast;
using Void = Entropy.Ext.Ast.Void;

namespace Entropy.Ext
{
    // ReSharper disable CanBeReplacedWithTryCastAndCheckForNull
    internal class DebugFormatter : IJsFormatter
 	{
 	    private readonly StringBuilder sb = new StringBuilder();
        private string indent = IndentSpace;
        private bool atNewLine = true;
	    private int nodeLevel;

	    const string IndentSpace = "    ";

	    private void IncreaseIndent()
		{
			indent += IndentSpace;
		}

 	    private void NewLine()
 	    {
 	        sb.AppendLine("");
 	        atNewLine = true;
 	    }

 	    private void DecreaseIndent()
		{
			indent  = indent.Substring(0, indent.Length - IndentSpace.Length);
		}

 	    void Put(string value)
 	    {
 	        if (atNewLine)
 	        {
				sb.Append(indent);
 	            atNewLine = false;
 	        }
 	        sb.Append(value);
		}

 	    private void WriteNode(Node node)
 	    {
	 	    nodeLevel++;

			if (node is Variable)
				WriteVar((Variable)node);
			else if (node is Call)
				WriteCall((Call)node);
			else if (node is PropertyGet)
                WritePropertyGet((PropertyGet)node);
            else if (node is PropertySet)
                WritePropertySet((PropertySet)node);
			else if (node is FunctionDefinition)
				WriteFunctionDefinition((FunctionDefinition) node);
			else if(node is Const)
				WriteConst((Const)node);
			else if(node is Conditional)
				WriteConditional((Conditional)node);
			else if(node is Binary)
				WriteBinary((Binary)node);
			else if(node is Unary)
                WriteUnary((Unary)node);
			else if(node is Param)
				WriteParam((Param)node);
		    else if (node is ArrayInit)
				WriteArrayInit((ArrayInit)node);
			else if (node is Json)
				WriteJson((Json)node);
		    else if (node is Reference)
			    WriteReference((Reference) node);
		    else if (node is Block)
			    WriteBlock((Block) node);
		    else if (node is Return)
				WriteReturn((Return)node);
		    else if (node is Label)
				WriteLabel((Label)node);
		    else if (node is Default)
				WriteDefault((Default)node);
			else
				throw new Exception("Unexpected " + node.Dump);
			
			nodeLevel--;
		}

	    private void WriteDefault(Default node)
	    {
	    }

	    private void WriteLabel(Label node)
	    {
	    }

	    private void WriteReturn(Return node)
	    {
			Put("return");
		    if (node.Value != Void.Value)
		    {
				Put(" ");
				WriteNode(node.Value);
				Put(";");
			}
	    }

	    private void WriteBlock(Block node)
	    {
			Put("{");
			IncreaseIndent();
			NewLine();
			foreach (var child in node.Children)
			{
				WriteNode(child);
			}
			DecreaseIndent();
			NewLine();
			Put("}");
		}

	    private void WriteParam(Param node)
	    {
			Put(node.Name);
	    }

        private void WriteUnary(Unary node)
        {
            switch (node.Operator)
            {
                case UnaryOperator.Not:
                    Put("!");
                    Put("(");
                    WriteNode(node.Operand);
                    Put(")");
                    break;
                
                case UnaryOperator.Convert:
                case UnaryOperator.Quote:
                    WriteNode(node.Operand);
                    break;
                default:
                    throw new Exception("Unexpected: " + node.Operator);

            }
        }

	    private void WriteBinary(Binary node)
	    {
		    WriteNode(node.Left);
			switch (node.Operator)
			{
				case BinaryOperator.Equal: 
					Put("==");
					break;
				case BinaryOperator.NotEqual: 
					Put("!=");
					break;
				case BinaryOperator.GreaterThan: 
					Put(">");
					break;
				case BinaryOperator.GreaterThanOrEqual: 
					Put(">=");
					break;
				case BinaryOperator.LessThan: 
					Put("<");
					break;
				case BinaryOperator.LessThanOrEqual: 
					Put("<=");
					break;
				case BinaryOperator.Add:
					Put("+");
					break;
				case BinaryOperator.AndAlso:
					Put(" && ");
					break;
				case BinaryOperator.OrElse:
					Put(" || ");
					break;
				case BinaryOperator.Assign:
					Put(" = ");
					break;
				case BinaryOperator.ArrayIndex:
					Put("[");
					break;
				default:
					throw  new Exception("Unexpected: " + node.Operator);
			}
			
			WriteNode(node.Right);
		    switch (node.Operator)
		    {
		        case BinaryOperator.Assign:
		            Put(";");
		            NewLine();
		            break;
                case BinaryOperator.ArrayIndex:
                    Put("]");
                    break;
            }
	    }

	    private void WriteConditional(Conditional node)
	    {
		    if (node.IfTrue is Block || node.IfFalse is Block)
		    {
				Put("if(");
				WriteNode(node.Test);
				Put(")");
				NewLine();
				WriteNode(node.IfTrue);
			    if (node.IfFalse != null)
			    {
					Put("else");
					NewLine();
					WriteNode(node.IfFalse);
				}
		    }
		    else
		    {
			    WriteNode(node.Test);
			    Put("?");
			    WriteNode(node.IfTrue);
			    Put(":");
			    WriteNode(node.IfFalse);
		    }
	    }

	    private void WriteConst(Const node)
	    {
			Put(node.Value);
	    }

	    private void WriteFunctionDefinition(FunctionDefinition node)
	    {
			Put("function");
			Put("(");
			bool isFirst = true;
			foreach (var param in node.Parameters)
			{
				if (isFirst)
					isFirst = false;
				else
					Put(", ");

				Put(param);
			}
			Put(")");
			NewLine();
			bool isLambda = !(node.Root is Block);
		    if (isLambda)
		    {
			    Put("{");
			    IncreaseIndent();
			    NewLine();
			    Put(" return ");
			    WriteNode(node.Root);
			    Put(";");
			    NewLine();
			    DecreaseIndent();
			    Put("}");
			    NewLine();
		    }
		    else
			    WriteNode(node.Root);
	    }


        private void WriteCall(Call node)
 	    {
 	        WriteNode(node.Object);
            Put(".");
            Put(node.FunctionName);
            Put("(");
 	        
            bool firstArg = true;
 	        foreach (var arg in node.Arguments)
 	        {
	 	        if (firstArg)
 	                firstArg = false;
 	            else
                    Put(", ");

	 	        WriteNode(arg);
 	        }
	 	    Put(")");
	        if (nodeLevel == 1) //  top-level call
	        {
		        Put(";");
				NewLine();
	        }
 	    }

        private void WritePropertyGet(PropertyGet node)
        {
		    WriteNode(node.Object);
		    Put(".");
	        Put(node.PropName);
        }

        private void WritePropertySet(PropertySet node)
        {
            Put(node.ObjectName);
            Put(".");
            Put(node.PropName);
            Put(" = ");
            WriteNode(node.Value);
        }


		private void WriteReference(Reference arg)
        {
	        Put(arg.Variable.Name);
        }

        private void WriteArrayInit(ArrayInit arg)
        {
            Put("[");
            bool firstArg = true;
            foreach (var elem in arg.Elems)
            {
 	            if (firstArg)
 	                firstArg = false;
 	            else
 	                Put(", ");
                
                WriteNode(elem);
            }
            Put("]");
        }

        private void WriteJson(Json arg)
        {
			IncreaseIndent();
			NewLine();
			Put("{");
			IncreaseIndent();
			bool firstArg = true;
			foreach (var child in arg.Props)
            {
				if (firstArg)
					firstArg = false;
				else
					Put(", ");
				NewLine();
                Put(child.Name);
                Put(": ");
                WriteNode(child.Value);
            }
			DecreaseIndent();
			NewLine();
			Put("}");
            DecreaseIndent();
 	    }

 	    private void WriteVar(Variable node)
 	    {
			Put("var " + node.Name);
 	    }

	    string IJsFormatter.Format(IEnumerable<Node> script)
 	    {
 	        sb.Clear();
		    
			foreach (var node in script)
			    WriteNode(node);
		    
			return sb.ToString();
        }
 	}
    // ReSharper restore CanBeReplacedWithTryCastAndCheckForNull
}